= {'filename': 'datasets/basics_example/basics_example',
metadata_dict 'resolution_in_microns': (1, 0.36, 0.36), # you can typically get this from the .tif metadata
'subsampling_factors': (1, 1/3, 1/3), # how much you subsampled your image for segmentation
}
Mesh creation and remeshing
igl
as backend
This notebook contains functions for (a) meshing (computing a mesh from a 3d segmentation) and (b) subdividing and simplifying meshes while preserving UV information. The latter is useful since it allows one to increase or decrease the mesh “resolution” as needed when the mesh is deformed.
Meshing using the marching cubes method
A key part of the tissue cartography pipeline is converting a 3d segmentation into a triangular mesh. We do this using the marching cube algorithm. It is essential to convert the mesh vertex coordinates from pixels into microns!
marching_cubes
marching_cubes (volume, isovalue=0.5, sigma_smoothing=0)
Compute triangular mesh of isosurface using marching cubes as implemented by lib|igl.
Type | Default | Details | |
---|---|---|---|
volume | 3d np.array | Array with scalar values from which to compute the isosurface. | |
isovalue | float | 0.5 | |
sigma_smoothing | int | 0 | |
Returns | np.array, np.array | vertices : np.array of shape (n_vertices, 3) Vertices faces : np.array of shape (n_faces, 3) Triangular faces (each face is a set of indices into the vertices array) |
Let’s test this on an example.
# load the segmentation created by ilastik (see notebook 01a)
= tcio.read_h5(f"{metadata_dict['filename']}_subsampled-image_Probabilities.h5")
segmentation = segmentation[0] # Select the first channel of the segmentation - it's the probability a pixel
segmentation # is part of the sample
print("segmentation shape:", segmentation.shape)
segmentation shape: (26, 151, 170)
= marching_cubes(segmentation, isovalue=0.5, sigma_smoothing=3)
vertices, faces
# EXTREMELY IMPORTANT - we now rescale the vertex coordinates so that they are in microns.
= vertices * (np.array(metadata_dict['resolution_in_microns'])
vertices_in_microns /np.array(metadata_dict['subsampling_factors']))
= tcmesh.ObjMesh(vertices_in_microns, faces)
mesh = "basics_example_mesh_marching_cubes_igl"
mesh.name f"{metadata_dict['filename']}_mesh_marching_cubes_igl.obj") mesh.write_obj(
Subdivision with igl
We’ll use the igl.upsample
function since it can cleanly deal with UV seams. The more sophisticated loop subdivision algorithm messes them up.
subdivide_igl
subdivide_igl (mesh, reglue=True, decimals=None)
*Refine mesh by edge subdivision using igl.
Subdivides all edges by placing new vertices at edge midpoints. Preserves UV information, by cutting the mesh along seams and (optionally) gluing back after. New texture vertices are also placed at texture-edge midpoints.*
Type | Default | Details | |
---|---|---|---|
mesh | ObjMesh | Initial mesh. | |
reglue | bool | True | Glue back after cutting |
decimals | NoneType | None | Decimal precision for merging vertices when regluing. If None, estimate from average edge mesh length as -4*log_10(avg length) |
Returns | ObjMesh | Subdivided mesh. |
= tcmesh.ObjMesh.read_obj("datasets/movie_example/initial_uv.obj")
mesh_test = subdivide_igl(mesh_test, reglue=True)
mesh_subdiv "datasets/movie_example/mesh_subdivided_igl.obj", ) mesh_subdiv.write_obj(
Warning: readOBJ() ignored non-comment line 4:
o mesh_01_cylinder_seams_uv
*mesh_subdiv.texture_vertices.T, mesh_subdiv.texture_tris, lw=0.2) plt.triplot(
Improve mesh quality by flips
We can change our mesh topology while keeping vertex positions fixed by doing edge flips to avoid deformed triangles. This can be done using the intrinsic Delaunay algorithm. By cutting/regluing the mesh, we can avoid flipping edges along the seam, thus preserving UV information.
Note: this can lead to self-overlaps in the UV-mapped triangulation. this can be fixed by a round of Laplacian smoothing of the UV coordinates.
make_delaunay
make_delaunay (mesh)
*Make mesh triangles less deformed by edge flips.
This algorithm improves mesh quality (i.e. makes triangles less deformed) without moving vertices by “edge flips” using the Delaunay algorithm. UV information is preserved by forbidding the flip of edges along the UV seams.
Note that this algorithm can lead to self-overlap of the UV map. You can fix this using wrapping.smooth_laplacian_texture.*
Type | Details | |
---|---|---|
mesh | ObjMesh | Initial mesh. |
Returns | ObjMesh | Mesh with flipped edges. |
= tcmesh.ObjMesh.read_obj(f"datasets/movie_example/meshes_wrapped/mesh_20_wrapped.obj")
mesh
= make_delaunay(mesh)
mesh_new f"datasets/movie_example/mesh_20_wrapped_delaunay.obj") mesh_new.write_obj(
*mesh_new.texture_vertices.T, mesh_new.texture_tris,
plt.tripcolor(0]) mesh_new.vertices[mesh_new.get_vertex_to_texture_vertex_indices(),
simplification/decimation with igl
Unfortunately, mesh simplification (qslim) in igl
degrades UV information: Assigning texture coordinates simply based on the birth vertex leads to “jagged” UV maps. Can we do better? I guess not :
qslim
qslim (mesh, max_n_faces)
*Simplify mesh by face decimation using the qslim algorithm.
A wrapper of igl.qslim. This will destroy UV mapping information!*
Type | Details | |
---|---|---|
mesh | ObjMesh | Initial mesh. |
max_n_faces | int | Maximum number of faces in output |
Returns | ObjMesh | Decimated mesh. |